using System.Collections;
using System.Collections.Generic;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.InputSystem;
using GameBasic.BehaviorTree;
using static GameBasic.BehaviorTree.Bhv;
using GameBasic;

namespace VCity
{
    public class CharCtrl : MonoBehaviour
    {
        public const float SkinWidth = 0.02f;

        //
        static int Anim_Idle;
        static int Anim_Run;
        static int Anim_Jump;
        static int Anim_Fall;

        public enum State
        {
            None = 0,
            Idle = 1,
            Run = 1 << 1,
            Jump = 1 << 2,
            Fall = 1 << 3,
            Fly = 1 << 4,
            All = -1
        }

        // Settings
        [Header("Physics Settings")]
        public float legHeight = 0.25f;
        public float collisionSphereRadius = 0.25f;

        [Space()]
        public float moveSpeed = 3f;
        public float flySpeed = 4;
        public float jumpSpeed = 4;
        public float maxFallSpeed = 5;

        [Header("Control Settings")]
        public float rotationPower = 0.5f;

        // Ref
        [Header("Ref")]
        public Transform followTransform;
        public SkinnedMeshRenderer meshObj;
        public Animator animator;

        // Runtime
        // ui
        private CursorState cursorState;
        // Input
        private InputAction movementAct;
        private InputAction jumpAct;
        private InputAction flyAct;
        private InputAction camZoomAct;
        float2 moveInputs;
        float2 lookInputs;
        bool jumpInput;
        float flyInput;
        float camZoomInput;

        // State
        State state;
        Bhv bhvRoot;
        Vector3 velocity;

        // Anim
        int animHash;
        float animTime;

        bool HasMoveInputs => moveInputs.x != 0 || moveInputs.y != 0;
        bool HasJumpInput => jumpInput;
        bool HasFlyInput => flyInput != 0;

        public int CurrentAnimHash => animHash;

        #region Unity Methods
        private void Awake()
        {
            Anim_Idle = Animator.StringToHash("Idle");
            Anim_Run = Animator.StringToHash("Run");
            Anim_Jump = Animator.StringToHash("Jump");
            Anim_Fall = Animator.StringToHash("Fall");
        }

        // Start is called before the first frame update
        void Start()
        {
            // Input
            var inputMain = InputMain.Instance;
            PlayerInput playerInput = inputMain.playerInput;
            movementAct = playerInput.actions["Movement"];
            jumpAct = playerInput.actions["Jump"];
            flyAct = playerInput.actions["Fly"];
            camZoomAct = playerInput.actions["CamZoom"];

            // 
            cursorState = new CursorState()
            {
                visible = false,
                lockMode = CursorLockMode.Locked
            };
            // drity solution: maybe login ui showed
            inputMain.cursorStates.Insert(1, cursorState);

            CreateBhvTree();

            // register
            //ClientMain.Instance.charCtrl = this;
        }

        private void OnDestroy()
        {
            var inputMain = InputMain.Instance;
            if (inputMain != null)
                inputMain.RemoveCursorState(cursorState);

            //var main = ClientMain.Instance;
            //if (main != null && main.charCtrl == this)
            //    ClientMain.Instance.charCtrl = null;
        }

        // Update is called once per frame
        void Update()
        {
            UpdateInputs();
            UpdateFollowTransform();
            UpdatePhysics();
            // clear velocity if not jumping or falling
            if (velocity.y == 0)
                velocity = Vector3.zero;

            // animator data
            var animState = animator.GetCurrentAnimatorStateInfo(0);
            animHash = animState.shortNameHash;
            animTime = animState.normalizedTime;

            // bhv
            bhvRoot.Update();

            // network
            //var netPlayer = NetPlayer.Instance;
            //if (netPlayer != null)
            //{
            //    netPlayer.transform.position = this.transform.position;
            //    netPlayer.transform.rotation = this.transform.rotation;
            //}
        }
        #endregion

        #region Inputs
        void UpdateInputs()
        {
            // Inputs
            // move
            moveInputs = movementAct.ReadValue<Vector2>();
            // rotate
            float2 mouseDelta = Mouse.current.delta.ReadValue();
            float2 screenSize = new float2(Screen.width, Screen.height);
            lookInputs = mouseDelta / screenSize;
            // jump
            jumpInput = jumpAct.triggered;
            // fly
            flyInput = flyAct.ReadValue<float>();
            // cam zoom
            camZoomInput = camZoomAct.ReadValue<float>();
        }
        #endregion

        #region Ctrl
        void UpdateFollowTransform()
        {
            followTransform.rotation *= Quaternion.AngleAxis(lookInputs.x * rotationPower * 180, Vector3.up);
            followTransform.rotation *= Quaternion.AngleAxis(-lookInputs.y * rotationPower * 180, Vector3.right);

            // Clamp the Up/Down rotation
            var angles = followTransform.localEulerAngles;
            angles.z = 0;
            if (angles.x > 180 && angles.x < 300)
            {
                angles.x = 300;
            }
            else if (angles.x > 60 && angles.x < 180)
            {
                angles.x = 60;
            }

            followTransform.localEulerAngles = angles;

            // cam zoom
            var vcams = VCams.Instance;
            if (camZoomInput != 0 && vcams != null)
            {
                bool isFirstPersonCam = vcams.isFirstPersonCam;
                if (!isFirstPersonCam)
                    vcams.UpdateCharCamZoom(-camZoomInput);
                meshObj.gameObject.SetActive(!(vcams.CharacterCamDistance == 0 || isFirstPersonCam));
            }
        }

        void UpdateCharDirection()
        {
            // Set player rotation based on the look transform
            transform.rotation = Quaternion.Euler(0, followTransform.rotation.eulerAngles.y, 0);
            // reset the y rotation of the look transform
            var angles = followTransform.localEulerAngles;
            followTransform.localEulerAngles = new Vector3(angles.x, 0, 0);
        }
        #endregion

        #region Physics
        // can be fixed update
        void UpdatePhysics()
        {
            float deltaTime = Time.deltaTime;

            // down cast
            Vector3 position = transform.position;

            if (state == State.Fly)
            {
                if (velocity != Vector3.zero)
                {
                    Vector3 movement = velocity * deltaTime;
                    float mag = movement.magnitude;
                    Vector3 movedir = movement / mag;

                    // hit any thing, stop move, start falling
                    if (SphereCast(position, movedir, out var hitInfo, math.max(mag, SkinWidth)))
                    {
                        //Debug.DrawLine(hitInfo.point, hitInfo.point + Vector3.forward, Color.red, 3);
                        //Debug.DrawLine(position, position + movedir * hitInfo.distance, Color.green, 3);
                        mag = Mathf.Max(hitInfo.distance - SkinWidth, 0);
                        //Debug.Log("Fly hit");
                    }

                    movement = movedir * mag;
                    position += movement;
                }
            }
            else if (velocity.y == 0)
            {
                Vector3 leg = Vector3.up * legHeight;
                Vector3 bodyPos = position + leg;
                RaycastHit hitInfo;

                // has velocity
                if (velocity != Vector3.zero)
                {
                    Vector3 movement = velocity * deltaTime;
                    float moveDist = movement.magnitude;
                    Vector3 moveDir = movement / moveDist;

                    // xz plane movement
                    if (SphereCast(bodyPos, moveDir, out hitInfo, math.max(moveDist, SkinWidth)))
                    {
                        moveDist = math.max(hitInfo.distance - SkinWidth, 0);
                    }

                    bodyPos += moveDir * moveDist;
                }

                // clamp to ground
                if (SphereCast(bodyPos, Vector3.down, out hitInfo, legHeight * 2))
                {
                    float downDist = hitInfo.distance - SkinWidth - collisionSphereRadius;
                    bodyPos += Vector3.down * downDist;
                }
                // start falling
                else
                {
                    velocity.y = -0.01f;
                }

                position = bodyPos - leg;
            }
            // jump or fall
            else
            {
                // gravity, fall
                velocity.y += Physics.gravity.y * deltaTime;
                velocity.y = math.max(velocity.y, -maxFallSpeed);

                // is falling?
                if (velocity.y < 0)
                {
                    bool downCastHit = SphereCast(position, Vector3.down, out var hitInfo, 1);
                    if (downCastHit)
                    {
                        if (hitInfo.distance <= SkinWidth)
                        {
                            velocity.y = 0;
                            //movement.y = 0;
                        }
                        else
                        {
                            //movement.y = math.max(movement.y, -(hitInfo.distance - SkinWidth));
                        }
                    }

                    // Fall end
                    if (velocity.y == 0)
                        velocity = Vector3.zero;
                }

                // Clamp movement
                if (velocity != Vector3.zero)
                {
                    Vector3 movement = velocity * deltaTime;
                    float mag = movement.magnitude;
                    Vector3 movedir = movement / mag;

                    // hit any thing, stop move, start falling
                    if (SphereCast(position, movedir, out var hitInfo, math.max(mag, SkinWidth)))
                    {
                        //Debug.DrawLine(hitInfo.point, hitInfo.point + Vector3.up, Color.red, 3);
                        mag = 0;

                        // start falling
                        velocity = new Vector3(0, -0.01f, 0);
                    }

                    movement = movedir * mag;
                    position += movement;
                }
            }

            transform.position = position;
        }

        bool SphereCast(Vector3 position, Vector3 dir, out RaycastHit hitInfo, float distance)
        {
            Vector3 origin = position + new Vector3(0, collisionSphereRadius, 0);
            var envLayer = PhysicsSettings.Instance.envLayer;

            return Physics.SphereCast(origin, collisionSphereRadius, dir, out hitInfo, distance, envLayer);
        }
        #endregion

        #region Animation
        void PlayAnim(int animHash)
        {
            animator.CrossFade(animHash, 0.05f, 0, 0);
            //if (this.animHash != animHash)
                //animator.CrossFade(animHash, 0.075f, 0, 0);
        }

        bool IsAnimEnd(int animHash)
        {
            return this.animHash == animHash && animTime >= 0.95f;
        }
        #endregion

        #region Bhv Tree
        void CreateBhvTree()
        {
            bhvRoot = Root(Priority(
                BhvFly(),
                Parallel(BhvJump, BhvFall, BhvRun), 
                BhvIdle()));
        }

        Bhv BhvIdle()
        {
            Bhv bhv = new Bhv("Idle");
            bhv.startCondition = () => state == State.None;
            bhv.stopCondition = () => state.Any(State.All, State.Idle);
            bhv.OnStart(() => {
                //Debug.Log("Idle start");
                PlayAnim(Anim_Idle);
                state |= State.Idle;
            });
            bhv.OnStop(() =>
            {
                state &= ~State.Idle;
            });

            return bhv;
        }

        Bhv BhvRun()
        {
            Bhv bhv = new Bhv("Run");
            bhv.startCondition = () => HasMoveInputs && !state.Any(State.All, State.Idle | State.Run);
            bhv.stopCondition = () => !HasMoveInputs || state.Any(State.All, State.Idle | State.Run);
            bhv.OnStart(() => {
                //Debug.Log("Run start");
                PlayAnim(Anim_Run);
                state |= State.Run;
            });
            bhv.OnUpdate(() =>
            {
                // move
                Vector3 move = moveSpeed * (moveInputs.y * transform.forward + moveInputs.x * transform.right);
                velocity.x = move.x;
                velocity.z = move.z;

                UpdateCharDirection();
            });
            bhv.OnStop(() =>
            {
                //Debug.Log("Run stop");
                state &= ~State.Run;
            });

            return bhv;
        }

        Bhv BhvFly()
        {
            Bhv bhv = new Bhv("Fly");
            bhv.startCondition = () => HasFlyInput;
            bhv.stopCondition = () => SphereCast(transform.position, Vector3.down, out var hitInfo, SkinWidth + 0.01f);

            bhv.OnStart(() => {
                state |= State.Fly;
                PlayAnim(Anim_Idle);
            });
            bhv.OnUpdate(() => {
                velocity = Vector3.zero;
                Vector3 dir = new Vector3(moveInputs.x, flyInput, moveInputs.y);
                if (dir != Vector3.zero)
                    velocity = flySpeed * (transform.rotation * dir.normalized);

                UpdateCharDirection();
            });
            bhv.OnStop(() =>
            {
                state &= ~State.Fly;
            });

            return bhv;
        }

        Bhv BhvJump()
        {
            // variables
            Bhv bhv = new Bhv("Jump");
            bhv.startCondition = () => HasJumpInput && velocity.y == 0 && !state.Any(State.Fly);
            //bhv.stopCondition = () => IsAnimEnd(Anim_Jump) || (IsAnimUnFix(Anim_Jump) && HasMoveInputs);
            bhv.stopCondition = () => velocity.y <= 0;
            bhv.OnStart(() => {
                //Debug.Log($"Jump start {Time.frameCount}");
                PlayAnim(Anim_Jump);
                state |= State.Jump;

                // Velocity
                velocity = Vector3.zero;
                if (HasMoveInputs)
                    velocity = Vector3.Normalize(moveInputs.y * transform.forward + moveInputs.x * transform.right) * moveSpeed;
                velocity.y = jumpSpeed;
            });
            bhv.OnStop(() =>
            {
                //Debug.Log($"Jump end {Time.frameCount}");
                state &= ~State.Jump;
            });

            return bhv;
        }

        Bhv BhvFall()
        {
            Bhv bhv = new Bhv("Fall");

            bhv.startCondition = () => velocity.y < 0 && !state.Any(State.Fly);
            bhv.stopCondition = () => velocity.y >= 0;
            bhv.OnStart(() =>
            {
                //Debug.Log("Fall start");
                PlayAnim(Anim_Fall);
                state |= State.Fall;
            });
            bhv.OnStop(() => {
                //Debug.Log("Fall end");
                state &= ~State.Fall;
            });

            return bhv;
        }

        #endregion
    }
}